/* Buffer cache.
 *
 * The buffer cache is a linked list of buf structures holding
 * cached copies of disk block contents.  Caching disk blocks
 * in memory reduces the number of disk reads and also provides
 * a synchronization point for disk blocks used by multiple processes.
 *
 * Interface:
 * * To get a buffer for a particular disk block, call bread.
 * * After changing buffer data, call bwrite to write it to disk.
 * * When done with the buffer, call brelse.
 * * Do not use the buffer after calling brelse.
 * * Only one process at a time can use a buffer,
 *     so do not keep them longer than necessary.
 *
 * The implementation uses two state flags internally:
 * * B_VALID: the buffer data has been read from the disk.
 * * B_DIRTY: the buffer data has been modified
 *     and needs to be written to disk.
 */

#include "spinlock.h"
#include "sleeplock.h"
#include "buf.h"
#include "console.h"
#include "sd.h"
#include "fs.h"
#include "string.h"

// #define BIO_TRACE

struct {
    struct spinlock lock;
    struct buf buf[NBUF];
    list_t list;
} bcache;

virtio_i virtio;

static void buf_clear(struct buf *b) {
    memset(b->data, 0, BSIZE);
    b->flags = 0;
    b->dev = 0;
    b->blockno = 0;
    b->refcnt = 0;
    b->started = 0;
}

static void buf_init(struct buf *b) {
    memset(b, 0, sizeof(struct buf));
    init_mpsc_node(&b->node);
    list_node_init(&b->bnode);
    initsleeplock(&b->lock, "buf.lock");
}

/* Initialize the cache list and locks. */
void
binit()
{
    cprintf("binit: bcache size: %d\n", NBUF);

    initlock(&bcache.lock, "bcache.lock");

    list_init(&bcache.list);
    for (int i = 0; i < NBUF; i++) {
        struct buf *b = bcache.buf + i;
        buf_init(b);
        list_push_back(&bcache.list, &b->bnode);
    }

    virtio.sync = sd_rw;
}

/*
 * Look through buffer cache for block on device dev.
 * If not found, allocate a buffer.
 * In either case, return locked buffer.
 */
static struct buf *
bget(uint32_t dev, uint32_t blockno)
{
    acquire(&bcache.lock);

    struct buf *b;
    list_node_t *x;
    for (x = bcache.list.head; x; x = x->next) {
        b = GET_BUF(x);
        if (b->dev == dev && b->blockno == blockno) {
            b->refcnt++;
            release(&bcache.lock);
            acquiresleep(&b->lock);
            return b;
        }
    }

    for (x = bcache.list.tail; x; x = x->prev) {
        b = GET_BUF(x);
        if (b->refcnt == 0) {
            assert(!(b->flags & B_DIRTY));

            b->refcnt = 1;
            b->dev = dev;
            b->blockno = blockno;
            b->flags = 0;
            release(&bcache.lock);
            acquiresleep(&b->lock);
            return b;
        }
    }

    panic("bget: run out of bcache.");
}

/* Return a locked buf with the contents of the indicated block. */
struct buf *
bread(uint32_t dev, uint32_t blockno)
{
#ifdef BIO_TRACE
    cprintf("cpu %d: bread: bno=%d\n", arm_cpuid(), blockno);
#endif

    struct buf *b = bget(dev, blockno);
    if (!(b->flags & B_VALID))
        virtio.sync(b);
    return b;
}

/* Write b's contents to disk. Must be locked. */
int bwrite(struct buf *b) {
#ifdef BIO_TRACE
    cprintf("cpu %d: bwrite: bno=%d\n", arm_cpuid(), b->blockno);
#endif

    assert(holdingsleep(&b->lock));
    return virtio.sync(b);
}

/*
 * Release a locked buffer.
 * Move to the head of the MRU list.
 */
void
brelse(struct buf *b)
{
#ifdef BIO_TRACE
    cprintf("cpu %d: brelse: bno=%d\n", arm_cpuid(), b->blockno);
#endif

    assert(holdingsleep(&b->lock));
    releasesleep(&b->lock);

    acquire(&bcache.lock);
    assert(b->refcnt > 0);
    b->refcnt--;
    if (b->refcnt == 0) {
        list_remove(&bcache.list, &b->bnode);
        list_push_front(&bcache.list, &b->bnode);
    }
    release(&bcache.lock);
}

void b_pin(struct buf *b) {
#ifdef BIO_TRACE
    cprintf("cpu %d: b_pin: bno=%d\n", arm_cpuid(), b->blockno);
#endif

    acquire(&bcache.lock);
    b->refcnt++;
    release(&bcache.lock);
}

void b_unpin(struct buf *b) {
#ifdef BIO_TRACE
    cprintf("cpu %d: b_unpin: bno=%d\n", arm_cpuid(), b->blockno);
#endif

    acquire(&bcache.lock);
    assert(b->refcnt > 0);
    b->refcnt--;
    release(&bcache.lock);
}

void b_invalidate() {
    acquire(&bcache.lock);

    list_node_t *x;
    for (x = bcache.list.head; x; x = x->next) {
        struct buf *b = GET_BUF(x);
        assert(b->refcnt == 0);
        buf_clear(b);
    }

    release(&bcache.lock);
}

void b_full_reset() {
    acquire(&bcache.lock);

    for (int i = 0; i < NBUF; i++) {
        buf_clear(bcache.buf + i);
    }

    release(&bcache.lock);
}